# -*- coding: utf-8 -*-

import configparser
import datetime
import logging
import os
import socket
import stat
import random
import re
import threading
import time

import daiquiri

from infclass_muter import base

daiquiri.setup(level=logging.DEBUG)
logger = daiquiri.getLogger(__name__)


class BadPermissions(Exception):
    pass


class InfclassMuter:

    def __init__(self, host, port, password):
        self.password = password
        self.host = host
        self.port = port
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.processors = []
        self.authenticated = False

    def connect(self):
        self.s.connect((self.host, self.port))

    def try_reconnect(self):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        while True:
            try:
                self.connect()
                return
            except socket.error as e:
                logger.error(str(e))
                time.sleep(10)

    def send(self, msg):
        self.s.send('{}\n'.format(msg).encode())

    def add_processor(self, processor):
        self.processors.append(processor)

    def run_messages(self):
        prev_run_truncated = None
        while True:
            try:
                data = self.s.recv(8096)
            except ConnectionError as e:
                # socket is dead; reconnect
                logger.warning('connection error: {}'.format(e))
                self.try_reconnect()
                continue

            if not data:
                # socket is dead; reconnect
                self.try_reconnect()
                continue
            try:
                msgs = data.decode('utf-8').split('\n\x00\x00')
            except UnicodeDecodeError:
                try:
                    msgs = data.decode('latin-1').split('\n\x00\x00')
                except Exception as e:
                    print(data)
                    raise
            # if there are many logs, it is possible that one of the messages
            # was received truncated at the previous recv() or at this one.
            if prev_run_truncated is not None:
                msgs[0] = prev_run_truncated + msgs[0]
                prev_run_truncated = None
            prev_run_truncated = msgs.pop()

            # now feed all logs to each processor
            for msg in msgs:
                # drop dead processors
                self.processors = [p for p in self.processors if p.alive]
                for p in self.processors:
                    try:
                        p.process(msg)
                    except Exception as e:
                        logger.exception('Error in {}'.format(p))


class Auth(base.Processor):
    def __init__(self, comm, password):
        super().__init__(comm)
        self.password = password

    def process(self, msg):
        if 'Enter password:' in msg:
            self.comm.send(self.password)
        self.comm.authenticated = True


class PlayerList(base.Processor):

    # example line:
    # '[Server]: (#18) test.nickname игрок          :
    #  [antispoof=0] [login=0] [level=0] [ip=64.32.16.8:4210]'
    exp = (r'\[Server\]: \(#(?P<id>\d+)\) (?P<name>.+)\s+:'
           r'(?:.*?)ip=(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
    # TODO: can we extract name here without catching spaces in the end?

    def __init__(self, communicator):
        super().__init__(communicator)
        self.comm = communicator
        self.stream_ongoing = False
        self.players = []
        self._players_in_progress = None
        self.pattern = re.compile(self.exp)
        self.timestamp = None
        self.refresh_sent_at = None

    def process(self, msg):
        if msg.startswith('[server]') and (
                msg.endswith("rcon='status'") or msg.endswith("cmd='status'")):
            # stream is going to start now
            logger.debug('status called: {}'.format(msg))
            self._players_in_progress = []
            return True

        res = self.pattern.search(msg)

        if res is None:
            # in addition to a typical line containing player info, `status`
            # can return a weird line for connecting player.
            # I don't know what to do with this line so i will just skip it.
            # It should be skipped without resetting temporary player list,
            # because it can be in the middle of the stream.
            # example:
            # [Server]: id=9 addr=1.2.4.8:1632 connecting
            if msg.startswith('[Server]') and msg.endswith('connecting'):
                return False

            # if there is no result, the stream of messages has
            # finished. And it means that it is time to replace
            # list of players
            if self._players_in_progress is not None:
                self.players = self._players_in_progress
                self.timestamp = datetime.datetime.now()
                logger.debug('Players: {}'.format(self.players))
                logger.info('Refreshed player list')

                logger.debug('resetting temp player list: "{}"'.format(msg))
                self._players_in_progress = None
            return False

        # at this point we have a match of a line containing player info.
        player = {
                'name': res.group('name').strip(),
                'id': int(res.group('id')),
                'ip': res.group('ip'),
                }
        self._players_in_progress.append(player)

    def get_player_list(self):
        return self.players

    def get_by_id(self, id):
        for player in self.get_player_list():
            if id == player['id']:
                return player

    def is_fresh(self):
        return True  # TODO: can't come up with good enough logic yet

    def refresh(self):
        self.refresh_sent_at = datetime.datetime.now()
        self.comm.send('status')


class MuteWatcher(base.Processor):

    # example line: [server]: ClientID=4 rcon='mute 04 1 test'
    # Attention: ClientID=4 is printed as decimal here!
    exp = (r"\[server\]: ClientID=(?P<muter>\d+) rcon='mute (?P<id>\d+) "
           r"(?P<time>\d+) (?P<reason>.*)'$")

    def __init__(self, communicator, player_list):
        super().__init__(communicator)
        self.comm = communicator
        self.player_list = player_list
        self.pattern = re.compile(self.exp)
    
    def process(self, msg):
        res = self.pattern.search(msg)
        if res is None:
            return False

        # get player from the list and create a maintainer for them
        player_id = int(res.group('id'))
        player = self.player_list.get_by_id(player_id)
        if player is None:
            # maybe we have an old list?
            # the problem here is that we cannot really get the list, because
            # player list processor will be called only at the next log line.
            # which means that we need to either create multithreaded thingy,
            # or do weird processing here in place. Lets just do nothing,
            # refresh player list and warn admins about it.
            self.player_list.refresh()
            self.comm.send('whisper {} please re-mute the player'.format(
                res.group('muter')))
            return True
            
        time = int(res.group('time'))
        reason = res.group('reason')
        _log = ('creating maintainer for {}, {}'
                ' minutes because of \'{}\''.format(
                    player['name'], time, reason))
        logger.info(_log)
        maintainer = MuteMaintainer(self.comm, player, time, reason)
        self.comm.add_processor(maintainer)
        return True


class MuteMaintainer(base.JoinProcessor):

    def __init__(self, communicator, player, time, reason):
        super().__init__(communicator)
        self.player = player
        self.time = time  # in minutes
        self.reason = reason

        self.names = set([self.player['name']])
        self.ips = set([self.player['ip']])

        self.created_at = datetime.datetime.now()

    def process(self, msg):
        res = self.pattern.search(msg)
        if res is None:
            return

        time_served = datetime.datetime.now() - self.created_at
        time_left = (datetime.timedelta(minutes=self.time) - time_served)

        if time_left > datetime.timedelta(0):
            ip = res.group('ip')
            if ip in self.ips:
                # prolong the mute
                player_id = int(res.group('id'), 16)  # id here is hex!
                minutes_left = int(time_left.total_seconds() / 60)
                logger.info("re-muting {} ({}), {} minutes for '{}' "
                            "(mute from {})".format(
                    self.player['name'], ip,  # original player nick is logged
                    minutes_left, self.reason, self.created_at))
                self.comm.send('mute {} {} {} [automaint]'.format(
                    player_id, minutes_left, self.reason))
        else:
            # the time has passed, lets mark the processor for removal
            logger.info('not remuting {}'.format(self.player['name']))
            self.alive = False


class WhitelistPopulator(base.JoinProcessor):

    LIVE_WHITELIST_PATH = os.path.expanduser('~/live_whitelist')

    def process(self, msg):
        res = self.pattern.search(msg)
        if res is None:
            return

        current_table = {}
        try:
            with open(self.LIVE_WHITELIST_PATH, 'rt') as f:
                for l in f.readlines():
                    parts = l.strip().split(',')
                    current_table[parts[1]] = {
                            'when': parts[0],
                            'ip': parts[1],
                            'type': parts[2]}
        except IOError:
            pass  # probably just does not exist

        new_now_hex = format(int(datetime.datetime.now().timestamp()), 'x')
        new_ip = res.group('ip')
        current_table[new_ip] = {
                'when': new_now_hex,
                'ip': new_ip,
                'type': 'infclass'}

        entries = []
        for e in current_table.values():
            when = datetime.datetime.fromtimestamp(int(e['when'], 16))
            if (datetime.datetime.now() - when).days <= 7:
                entries.append('{when},{ip},{type}'.format(**e))
        with open(self.LIVE_WHITELIST_PATH, 'wt') as f:
            f.write('\n'.join(entries))
        logger.info('Whitelisted entered ip {}'.format(new_ip))


class FunroundLimiter(base.Processor): 
    # example:
    # [5e023151][Console]: start_fun_round
    exp = (r'\[Console\]: start_fun_round')
    vote_name = 'Fun round'

    def __init__(self, communicator):
        super().__init__(communicator)
        self.comm = communicator
        self.timer = None
        self.pattern = re.compile(self.exp)

    def process(self, msg):
        res = self.pattern.search(msg)
        if res is None:
            return

        send_cmd = 'remove_vote "{}"'.format(self.vote_name)
        self.comm.send(send_cmd)

        # wait_minutes = int(res.group('wait_minutes'))
        wait_minutes = 15  # hardcoded for now
        wait_time = wait_minutes * 60
        self.timer = threading.Timer(wait_time, self.re_add,
                                     args=[wait_minutes])
        self.timer.start()
        send_cmd = 'say Fun round will be available in {} minutes'
        send_cmd = send_cmd.format(wait_minutes)
        self.comm.send(send_cmd)
        logger.info('Removed fun round')

    def re_add(self, wait_minutes):
        # wait_time is not used for now
        vote_cmd = 'echo start_fun_round; start_fun_round'
        send_cmd = 'add_vote "{}" "{}"'.format(self.vote_name, vote_cmd)
        self.comm.send(send_cmd)
        logger.info('Re-added fun round')


class PeriodicMessages:

    def __init__(self, comm):
        self.comm = comm
        self.schedule_print(0.1)  # auth should finish by then

    def schedule_print(self, wait_minutes=10):
        wait_time = wait_minutes * 60
        self.timer = threading.Timer(wait_time, self.print_message)
        self.timer.start()

    def print_message(self):
        if not self.comm.authenticated:
            logger.warning('not authenticated yet')
            self.schedule_print(60)
            return

        try:
            filenames = list(os.walk('phrases'))[0]
        except IndexError:
            logger.warning('create directory `phrases` with text files in it')
            self.schedule_print(60)
            return

        try:
            file_to_print = random.choice(filenames[2])
        except IndexError:
            logger.warning('create some text files in `phrases`')
            self.schedule_print(60)
            return

        with open('/'.join([filenames[0], file_to_print])) as f:
            lines = f.readlines()
            try:
                self.comm.send('say **************************')
                for l in lines:
                    self.comm.send('say {}'.format(l.strip()))
                self.comm.send('say **************************')
            except BrokenPipeError as e:
                logger.warning('could not print a phrase due to network error')

        self.schedule_print()


class SendStatus:

    def __init__(self, comm):
        self.comm = comm
        self.schedule_send(0.1)  # auth should finish by then

    def schedule_send(self, wait_minutes=1):
        wait_time = wait_minutes * 60
        self.timer = threading.Timer(wait_time, self.print_message)
        self.timer.start()

    def print_message(self):
        try:
            self.comm.send('status')
        except BrokenPipeError as e:
            logger.warning('could not print a phrase due to network error')
        self.schedule_send()


def check_config_permissions(path):
    m = os.stat(path).st_mode
    if m & (stat.S_IRWXG | stat.S_IRWXO) != 0:
        raise BadPermissions('Permissions for {} should be 600'.format(path))


def main():
    # fetch config
    path = os.path.expanduser('~/.infclass_econ_config.ini')
    check_config_permissions(path)
    config = configparser.ConfigParser()
    config.read([path])
    ip = config['DEFAULT']['ip']
    port = config.getint('DEFAULT', 'port')
    password = config['DEFAULT']['password']

    muter = InfclassMuter(ip, port, password)
    player_list = PlayerList(muter)
    periodic_messages = PeriodicMessages(muter)
    status_sender = SendStatus(muter)
    muter.add_processor(player_list)
    muter.add_processor(MuteWatcher(muter, player_list))
    muter.add_processor(Auth(muter, password))
    muter.add_processor(FunroundLimiter(muter))
    muter.add_processor(WhitelistPopulator(muter))
    muter.connect()
    muter.run_messages()


if __name__ == '__main__':
    main()
